using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NTrace;

namespace NTrace.Tasks
{
    public class EtwTrace : Task
    {
        private ITaskItem[] sources;
        private ITaskItem[] ignore;
        private ITaskItem[] traceLevels;
        private ITaskItem[] traceFlags;
        private List<ITaskItem> results;
        private List<ITaskItem> traceStubs;
        private String outputFolder;
        private String etwProviderVariable;
        private Guid providerId;
        private String friendlyName = String.Empty;
        private int functionTraceFlag = 0;
        private byte functionTraceLevel = 0;
        private String traceClassName = "EtwTrace";
        private String traceLevelName = "EtwTraceLevel";
        private String traceFlagName = "EtwTraceFlag";
        private String buildFlavor = "Debug";

        public EtwTrace()
        {
            this.traceStubs = new List<ITaskItem>();
        }

        public ITaskItem[] Ignore
        {
            get
            {
                return this.ignore;
            }
            set
            {
                this.ignore = value;
            }
        }

        [Required]
        public ITaskItem[] Sources
        {
            get
            {
                return this.sources;
            }
            set
            {
                this.sources = value;
            }
        }

        public ITaskItem[] TraceLevels
        {
            get
            {
                return this.traceLevels;
            }
            set
            {
                this.traceLevels = value;
            }
        }

        public ITaskItem[] TraceFlags
        {
            get
            {
                return this.traceFlags;
            }
            set
            {
                this.traceFlags = value;
            }
        }

        [Output]
        public ITaskItem[] Results
        {
            get
            {
                return this.results.ToArray();
            }
        }

        [Output]
        public ITaskItem[] TraceStubs
        {
            get
            {
                return this.traceStubs.ToArray();
            }
        }

        public String OutputFolder
        {
            get
            {
                return this.outputFolder;
            }
            set
            {
                this.outputFolder = value;
            }
        }

        [Required]
        public String EtwProviderVariable
        {
            get
            {
                return this.etwProviderVariable;
            }
            set
            {
                this.etwProviderVariable = value;
            }
        }

        [Required]
        public String ProviderId
        {
            get
            {
                return this.providerId.ToString("D");
            }
            set
            {
                this.providerId = new Guid(value);
            }
        }

        [Required]
        public String FriendlyName
        {
            get
            {
                return this.friendlyName;
            }
            set
            {
                this.friendlyName = value;
            }
        }

        public int FunctionTraceFlag
        {
            get
            {
                return this.functionTraceFlag;
            }
            set
            {
                this.functionTraceFlag = value;
            }
        }

        public Byte FunctionTraceLevel
        {
            get
            {
                return this.functionTraceLevel;
            }
            set
            {
                this.functionTraceLevel = value;
            }
        }

        public String TraceClassName
        {
            get
            {
                return this.traceClassName;
            }
            set
            {
                this.traceClassName = value;
            }
        }

        public String TraceLevelName
        {
            get
            {
                return this.traceLevelName;
            }
            set
            {
                this.traceLevelName = value;
            }
        }

        public String TraceFlagName
        {
            get
            {
                return this.traceFlagName;
            }
            set
            {
                this.traceFlagName = value;
            }
        }

        public String BuildFlavor
        {
            get
            {
                return this.buildFlavor;
            }
            set
            {
                this.buildFlavor = value;
            }
        }

        public override bool Execute()
        {
            bool output = true;
            List<String> sourceFiles = new List<String>();
            List<String> ignoreFiles = new List<String>();
            this.results = new List<ITaskItem>();

            List<String> processedFiles = new List<String>();

            NTrace.Environment environment = new NTrace.Environment();
            environment.OutputFolder = this.OutputFolder;
            environment.EtwProviderVariable = this.EtwProviderVariable;
            environment.ProviderId = this.providerId;
            environment.FriendlyName = this.FriendlyName;
            environment.FunctionTraceFlag = this.FunctionTraceFlag;
            environment.FunctionTraceLevel = this.FunctionTraceLevel;
            environment.TraceClassName = this.TraceClassName;
            environment.TraceLevelName = this.TraceLevelName;
            environment.TraceFlagName = this.TraceFlagName;
            environment.BuildFlavor = this.BuildFlavor;

            if (this.sources != null)
            {
                sourceFiles.AddRange(from src in this.sources select src.GetMetadata("FullPath"));
            }

            if (this.ignore != null)
            {
                ignoreFiles.AddRange(from ig in this.ignore select ig.GetMetadata("FullPath"));
            }

            if (this.TraceLevels != null)
            {
                output &= this.ReadTraceLevels(environment);
            }

            if (this.TraceFlags != null)
            {
                output &= this.ReadTraceFlags(environment);
            }

            if (output)
            {
                FileProcessor processor = new FileProcessor(environment);
                foreach (String targetFile in sourceFiles)
                {
                    if (ignoreFiles.Contains(targetFile))
                    {
                        this.Log.LogMessage(MessageImportance.Low, "Skipping file '{0}'", targetFile);
                        results.Add(new TaskItem(targetFile));
                    }
                    else
                    {
                        this.Log.LogMessage(MessageImportance.Low, "Processing file '{0}'", targetFile);
                        try
                        {
                            String preprocessedFile = processor.Process(targetFile);
                            results.Add(new TaskItem(preprocessedFile));
                        }
                        catch (NTrace.PreprocessorException e)
                        {
                            this.Log.LogError(
                                "NTrace",
                                e.ErrorCode,
                                String.Empty,
                                e.File,
                                e.StartLine,
                                e.StartColumn,
                                e.EndLine,
                                e.EndColumn,
                                e.Message);
                            output = false;
                        }
                    }
                }

                // Generate the Visual Studio trace stubs so Intellisense shows our configured flags/levels and the trace macro
                IEnumerable<String> traceStubs = this.GenerateTraceStubs(environment);
                this.traceStubs.AddRange((from ts in traceStubs select new TaskItem(ts)).ToArray());
            }

            return output;
        }

        private bool ReadTraceLevels(NTrace.Environment environment)
        {
            environment.Configuration.TraceLevels.Clear();
            bool output = true;
            var traceLevels = from tl in this.TraceLevels select new { Name = tl.ItemSpec, Value = tl.GetMetadata("Value") };
            foreach (var suppliedTraceLevel in traceLevels)
            {
                if (String.IsNullOrEmpty(suppliedTraceLevel.Value))
                {
                    System.Diagnostics.Debug.Assert(false, "We require a value for each trace level.");
                    this.Log.LogError("A value was not supplied for TraceLevel '{0}'.", suppliedTraceLevel.Name);
                    output = false;
                    break;
                }
                Byte levelValue;
                if (Byte.TryParse(suppliedTraceLevel.Value, out levelValue))
                {
                    environment.Configuration.TraceLevels[suppliedTraceLevel.Name] = levelValue;
                }
                else 
                {
                    this.Log.LogError("An invalid value was supplied for TraceLevel '{0}'; the valid range of values is 0-255.", suppliedTraceLevel.Name);
                }
            }
            return output;
        }

        private bool ReadTraceFlags(NTrace.Environment environment)
        {
            environment.Configuration.TraceFlags.Clear();
            bool output = true;
            var traceFlags = from tl in this.TraceFlags select new { Name = tl.ItemSpec, Value = tl.GetMetadata("Value") };
            foreach (var suppliedTraceFlag in traceFlags)
            {
                if (String.IsNullOrEmpty(suppliedTraceFlag.Value))
                {
                    System.Diagnostics.Debug.Assert(false, "We require a value for each trace flag.");
                    output = false;
                    break;
                }
                Int32 flagValue;
                if (Int32.TryParse(suppliedTraceFlag.Value, out flagValue))
                {
                    environment.Configuration.TraceFlags[suppliedTraceFlag.Name] = flagValue;
                }
                else
                {
                    this.Log.LogError("An invalid value was supplied for TraceFlag '{0}'; the value must be a valid 32-bit integer.", suppliedTraceFlag.Name);
                }
                environment.Configuration.TraceFlags[suppliedTraceFlag.Name] = flagValue;
            }
            return output;
        }

        private IEnumerable<String> GenerateTraceStubs(NTrace.Environment environment)
        {
            List<String> output = new List<String>();

            String tempPath = System.IO.Path.GetTempPath();
            String ntraceTempPath = System.IO.Path.Combine(tempPath, "NTrace");
            if (!System.IO.Directory.Exists(ntraceTempPath)) System.IO.Directory.CreateDirectory(ntraceTempPath);

            System.IO.Stream etwTraceStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("NTrace.Tasks.EtwTrace.cs");
            System.Diagnostics.Debug.Assert(etwTraceStream != null, "Expected a non-null stream for the EtwTrace.cs resource.");
            String etwTraceFilePath = System.IO.Path.Combine(ntraceTempPath, "EtwTrace.cs");
            using (System.IO.FileStream etwTraceFile = new System.IO.FileStream(etwTraceFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.Read))
            {
                CopyStream(etwTraceStream, etwTraceFile);
            }
            output.Add(etwTraceFilePath);

            String traceFlagsFilePath = System.IO.Path.Combine(ntraceTempPath, "EtwTraceFlags.cs");
            using (System.IO.TextWriter traceFlagsFile = new System.IO.StreamWriter(traceFlagsFilePath, false))
            {
                GenerateEnumerationFile(traceFlagsFile, environment.TraceFlagName, environment.Configuration.TraceFlags, true);
            }
            output.Add(traceFlagsFilePath);

            String traceLevelsFilePath = System.IO.Path.Combine(ntraceTempPath, "EtwTraceLevels.cs");
            using (System.IO.TextWriter traceLevelsFile = new System.IO.StreamWriter(traceLevelsFilePath, false))
            {
                GenerateEnumerationFile(traceLevelsFile, environment.TraceLevelName, environment.Configuration.TraceLevels);
            }
            output.Add(traceLevelsFilePath);

            return output.AsReadOnly();
        }
#if !NET40
        private static void GenerateEnumerationFile<T>(System.IO.TextWriter traceFlagsFile, String enumName, IDictionary<String, T> flagsDictionary)
        {
            EtwTrace.GenerateEnumerationFile<T>(traceFlagsFile, enumName, flagsDictionary, false);
        }

        private static void GenerateEnumerationFile<T>(System.IO.TextWriter traceFlagsFile, String enumName, IDictionary<String, T> flagsDictionary, bool isFlagsEnum)
#else
        private static void GenerateEnumerationFile<T>(System.IO.TextWriter traceFlagsFile, String enumName, IDictionary<String, T> flagsDictionary, bool isFlagsEnum = false)
#endif
        {
            Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
            System.CodeDom.Compiler.ICodeGenerator codeGenerator = codeProvider.CreateGenerator();
            System.CodeDom.CodeCompileUnit compileUnit = new System.CodeDom.CodeCompileUnit();
            System.CodeDom.CodeNamespace codeNamespace = new System.CodeDom.CodeNamespace();
            codeNamespace.Imports.Add(new System.CodeDom.CodeNamespaceImport("System"));
            
            System.CodeDom.CodeTypeDeclaration enumerationDeclaration = new System.CodeDom.CodeTypeDeclaration(enumName);
            enumerationDeclaration.IsEnum = true;
            if (isFlagsEnum)
                enumerationDeclaration.CustomAttributes.Add(new System.CodeDom.CodeAttributeDeclaration("Flags"));
            foreach (String flagName in flagsDictionary.Keys)
            {
                System.CodeDom.CodeMemberField member = new System.CodeDom.CodeMemberField();
                member.Name = flagName;
                member.InitExpression = new System.CodeDom.CodePrimitiveExpression(flagsDictionary[flagName]);
                enumerationDeclaration.Members.Add(member);
            }
            codeNamespace.Types.Add(enumerationDeclaration);
            compileUnit.Namespaces.Add(codeNamespace);
            codeGenerator.GenerateCodeFromCompileUnit(compileUnit, traceFlagsFile, new System.CodeDom.Compiler.CodeGeneratorOptions());
        }

        private static void CopyStream(System.IO.Stream source, System.IO.FileStream destination)
        {
            Byte[] buffer = new Byte[4096];
            for (
                int bytesRead = source.Read(buffer, 0, buffer.Length);
                bytesRead > 0;
                bytesRead = source.Read(buffer, 0, buffer.Length))
                destination.Write(buffer, 0, bytesRead);
        }

    }
}
